/*
 * Copyright (c) 2015, the DeltaSync# project. All Rights Reserved.
 * This project is a C# port of the JDeltaSync project:
 *     http://jdeltasync.googlecode.com
 * 
 * Licensed under the Apache License, Version 2.0 (the "License");
 * you may not use this file except in compliance with the License.
 * You may obtain a copy of the License at
 *
 *     http://www.apache.org/licenses/LICENSE-2.0
 *
 * Unless required by applicable law or agreed to in writing, software
 * distributed under the License is distributed on an "AS IS" BASIS,
 * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
 * See the License for the specific language governing permissions and
 * limitations under the License.
 */
 
//#define DONOTUSE_MimeKit

using System;
using System.Collections.Generic;
using System.IO;
using System.Security;

#if !DONOTUSE_MimeKit
using MimeKit;
#endif

namespace PM.Net.DeltaSync {
    /**
     *
     */
    public delegate T DeltaSyncMessageParser<T>(Stream data);

    public class MessageNotAvailableExeption : DeltaSyncException {
        public MessageNotAvailableExeption(string message) 
            : base(message)
        {}
        
        public MessageNotAvailableExeption(string message, Exception cause)
            : base(message, cause)
        {}
    }
    
    public class DeltaSyncClientHelper {
        private static Dictionary<string, string> STANDARD_FOLDERS_MAPPINGS;
        
        /**
         * The default number of messages to request at a time in 
         * {@link #getMessages(Folder)}. Max value seems to be 2000. If a higher 
         * value is used the server never returns more than 2000 in each Sync 
         * response.
         * <p>
         * Each Add in the response is about 1 kB so 256 should mean each 
         * response is about 256 kB maximum.
         */
        public static int DEFAULT_WINDOW_SIZE = 256;
        
        static DeltaSyncClientHelper() {
            STANDARD_FOLDERS_MAPPINGS = new Dictionary<string, string>();
            STANDARD_FOLDERS_MAPPINGS.Add("ACTIVE",       "Inbox");
            STANDARD_FOLDERS_MAPPINGS.Add("drAfT",        "Drafts");
            STANDARD_FOLDERS_MAPPINGS.Add("HM_BuLkMail_", "Junk");
            STANDARD_FOLDERS_MAPPINGS.Add("sAVeD",        "Sent");
            STANDARD_FOLDERS_MAPPINGS.Add("trAsH",        "Deleted");
            STANDARD_FOLDERS_MAPPINGS.Add(".!!OIM",       "Offline Instant Messages");
        }
    
        private DeltaSyncClient  client;
        private Store            store;
        private DeltaSyncSession session;
    
        private int windowSize = DEFAULT_WINDOW_SIZE;
        
        public DeltaSyncClientHelper() 
            : this(null, null)
        {}
        
        public DeltaSyncClientHelper(Store store) 
            : this(null, store)
        {}

        public DeltaSyncClientHelper(DeltaSyncClient client, Store store) {
            this.client   = client != null ? client : new DeltaSyncClient();
            this.store    = store  != null ? store  : new InMemoryStore();
        }
        
        ~DeltaSyncClientHelper() {
            Dispose(false);
        }
        public void Dispose() {
            Dispose(true);
            GC.SuppressFinalize(this);
        }
        
        protected virtual void Dispose(bool disposing) {
            if (disposing)
                if (this.client != null)
                    this.client.Dispose();
            this.client = null;
        }

        public void Close() {
            this.Dispose();
        }

        #region Properties...
        protected DeltaSyncSession Session {
            get { return session; }
        }
        
        public TimeSpan Timeout {
            get { return this.client.Timeout;  }
            set { this.client.Timeout = value; }
        }
        
        public TimeSpan SessionTimeout {
            get { return this.client.SessionTimeout;  }
            set { this.client.SessionTimeout = value; }
        }
        
        /**
         * Returns the current <code>windowSize</code> which specifies the maximum 
         * number of {@link Command} returned by a call to 
         * {@link DeltaSyncClient#sync(DeltaSyncSession, SyncRequest)} made by
         * {@link #getMessages(Folder)}. {@link #getMessages(Folder)} needs to do
         * <code>totalNumberOfMessagesInFolder / windowSize</code> requests to
         * get all messages in a folder.
         * 
         * @return the current <code>windowSize</code>.
         * @see #DEFAULT_WINDOW_SIZE
         */
        public int WindowSize {
            get { return windowSize; }
            set {
                if (value <= 0)
                    throw new ArgumentException("Invalid windowSize", "windowSize");
                this.windowSize = value;
            }
        }
        #endregion
        
        /**
         * Returns the <code>DisplayName</code> of a folder mapped to a nicer name. 
         * The standard folders have funny display names (e.g. drAfT).
         */    
        private string getMappedDisplayName(string displayName) {
            if (STANDARD_FOLDERS_MAPPINGS.ContainsKey(displayName)) {
                return STANDARD_FOLDERS_MAPPINGS[displayName];
            }
            return displayName;
        }
        
        private void checkLoggedIn() {
            if (client == null)
                throw new ObjectDisposedException(this.GetType().FullName);
            if (session == null)
                throw new InvalidOperationException("Not logged in");
        }
        
        /**
         * Logs in. 
         * 
         * @throws AuthenticationException if authentication fails.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if already logged in.
         */
        public void Login(string username, string password) {
            this.session = client.Login(username, password);
        }
        
        public void Login(string username, SecureString password) {
            this.session = client.Login(username, password);
        }
        
        /**
         * Returns all {@link Folder}s.
         * 
         * @return all {@link Folder}s.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public Folder[] Folders {
            get {
                checkLoggedIn();
                try {
                    return doGetFolders();
                } catch (SessionExpiredException) {
                    session = client.Renew(session);
                    return doGetFolders();
                } catch (InvalidSyncKeyException) {
                    //session.getLogger().warn("Invalid folders sync key. All folders " 
                    //        + "will be retrieved anew.");
                    store.ResetFolders(session.Credential.UserName);
                    return doGetFolders();
                }
            }
        }
    
        private Folder[] doGetFolders() {
            while (true) {
                SyncRequest syncRequest = new SyncRequest(new SyncRequest.Collection(
                        store.GetFoldersSyncKey(session.Credential.UserName), Clazz.Folder, true));
                SyncResponse response = client.sync(session, syncRequest);
                
                if (response.Collections.Count == 0)
                    throw new DeltaSyncException("No <Collection> in Sync response");
                SyncResponse.Collection collection = response.Collections[0];
                if (collection.Status != 1) {
                    throw new DeltaSyncException("Sync request failed with status " 
                            + collection.Status);
                }
            
                List<Folder> added   = new List<Folder>();
                List<string> deleted = new List<string>();
                foreach (Command cmd in collection.Commands) {
                    if (cmd is FolderAddCommand) {
                        FolderAddCommand addCmd = (FolderAddCommand) cmd;
                        added.Add(new Folder(addCmd.Id, getMappedDisplayName(addCmd.DisplayName)));
                    } else if (cmd is FolderDeleteCommand) {
                        FolderDeleteCommand delCmd = (FolderDeleteCommand) cmd;
                        deleted.Add(delCmd.Id);
                    }
                }
                
                store.UpdateFolders(session.Credential.UserName, collection.SyncKey, added, deleted);
                
                if (!collection.HasMoreAvailable) {
                    break;
                }
            }
    
            List<Folder> folders = store.GetFolders(session.Credential.UserName);
            return folders.ToArray();
        }
        
        /**
         * Returns the Inbox {@link Folder}.
         * 
         * @return the Inbox {@link Folder} or <code>null</code> if not found.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public Folder Inbox {
            get {
                checkLoggedIn();
                
                foreach (Folder folder in store.GetFolders(session.Credential.UserName)) {
                    if ("Inbox" == folder.Name)
                        return folder;
                }
                
                Folder[] dummy = this.Folders;
                foreach (Folder folder in store.GetFolders(session.Credential.UserName)) {
                    if ("Inbox" == folder.Name)
                        return folder;
                }
                return null;
            }
        }
        
        public Folder GetFolder(string name) {
            if (string.IsNullOrEmpty(name))
                throw new ArgumentNullException("FolderName");
            checkLoggedIn();
            
            foreach (Folder folder in store.GetFolders(session.Credential.UserName)) {
                if (name == folder.Name)
                    return folder;
            }
            
            Folder[] dummy = this.Folders;
            foreach (Folder folder in store.GetFolders(session.Credential.UserName)) {
                if (name == folder.Name)
                    return folder;
            }
            return null;
        }
        
        
        /**
         * Returns all messages in the specified {@link Folder}.
         * 
         * @param folder the {@link Folder}.
         * @return all messages in the specified {@link Folder}.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public Message[] GetMessages(Folder folder) {
            if (folder == null)
                throw new ArgumentNullException("Folder");
            checkLoggedIn();
            try {
                return doGetMessages(folder);
            } catch (SessionExpiredException) {
                session = client.Renew(session);
                return doGetMessages(folder);
            } catch (InvalidSyncKeyException) {
                //session.getLogger().warn("Invalid messages sync key. All messages " 
                //        + "will be retrieved anew.");
                store.ResetMessages(session.Credential.UserName, folder);
                return doGetMessages(folder);
            } catch (DeltaSyncException ex) {
                if (ex.Message.IndexOf("Sync request failed with status 4104") >= 0) {
                    //session.getLogger().warn("Got 4104 error. All messages " 
                    //        + "will be retrieved anew.");
                    store.ResetMessages(session.Credential.UserName, folder);
                    return doGetMessages(folder);
                }
                throw ex;
            }
        }
    
        private Message[] doGetMessages(Folder folder) {
            while (true) 
            {
                SyncRequest  syncRequest = new SyncRequest(new SyncRequest.Collection(
                        store.GetMessagesSyncKey(session.Credential.UserName, folder), Clazz.Email, folder.Id, true, windowSize));
                SyncResponse response    = client.sync(session, syncRequest);
                
                if (response.Collections.Count == 0)
                    throw new DeltaSyncException("No <Collection> in Sync response");
                SyncResponse.Collection collection = response.Collections[0];
                if (collection.Status != 1)
                    throw new DeltaSyncException("Sync request failed with status " 
                            + collection.Status);
                
                List<Message> added   = new List<Message>();
                List<string>  deleted = new List<string>();
                foreach (Command cmd in collection.Commands) {
                    if (cmd is EmailAddCommand) {
                        EmailAddCommand addCmd = (EmailAddCommand) cmd;
                        added.Add(new Message(
                            addCmd.Id,
                            addCmd.DateReceived, addCmd.Size, addCmd.IsRead, 
                            addCmd.Subject,      addCmd.From, addCmd.HasAttachments));
                    } else if (cmd is EmailDeleteCommand) {
                        EmailDeleteCommand delCmd = (EmailDeleteCommand) cmd;
                        deleted.Add(delCmd.Id);
                    }
                }
                
                store.UpdateMessages(session.Credential.UserName, folder, collection.SyncKey, added, deleted);
                
                if (!collection.HasMoreAvailable) {
                    break;
                }
            }
    
            List<Message> messages = store.GetMessages(session.Credential.UserName, folder);
            return messages.ToArray();
        }
    
        /**
         * Deletes the specified {@link Message} from the specified {@link Folder}.
         * 
         * @param folder the {@link Folder}.
         * @param message the {@link Message} to be deleted.
         * @return the true is the {@link Message}s was successfully deleted.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public bool Delete(Folder folder, Message message) {
            if (message == null)
                throw new ArgumentNullException("Message");
            string[] ids = new string[1] { message.Id };
            return Delete(folder, ids).Length > 0;
        }
        
        /**
         * Deletes the specified {@link Message}s from the specified {@link Folder}.
         * 
         * @param folder the {@link Folder}.
         * @param messages the {@link Message}s to be deleted.
         * @return the ids of the {@link Message}s actually deleted.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public string[] Delete(Folder folder, Message[] messages) {
            string[] ids = new string[messages.Length];
            for (int i = 0; i < messages.Length; i++) {
                if (messages[i] == null)
                    throw new ArgumentNullException("Message");
                ids[i] = messages[i].Id;
            }
            return Delete(folder, ids);
        }
        
        /**
         * Deletes the {@link Message}s with the specified ids from the specified 
         * {@link Folder}.
         * 
         * @param folder the {@link Folder}.
         * @param ids the ids to be deleted.
         * @return the ids of the {@link Message}s actually deleted.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public string[] Delete(Folder folder, string[] ids) {
            checkLoggedIn();
            List<string> idsList = new List<string>(ids);
            List<string> deleted = new List<string>();
            try {
                doDelete(folder, idsList, deleted);
            } catch (SessionExpiredException) {
                session = client.Renew(session);
                doDelete(folder, idsList, deleted);
            } catch (InvalidSyncKeyException) {
                //session.getLogger().debug("Invalid messages sync key. Delete will " 
                //        + "be retried with sync key 0");
                store.ResetMessages(session.Credential.UserName, folder);
                doDelete(folder, idsList, deleted);
            }
            return deleted.ToArray();
        }
        
        private void doDelete(Folder folder, List<string> ids, List<string> deleted) {
            while (ids.Count > 0) {
                /*
                 * Only delete up to 64 messages in each request. We don't know the 
                 * exact limit but we know that 160 is too many. 64 works fine.
                 */
                List<Command> commands = new List<Command>();
                int count = ids.Count < 64 ? ids.Count : 64;
                for (int index = 0; index < count; index++) {
                    string id = ids[index];
                    commands.Add(new EmailDeleteCommand(id));
                }
                
                SyncRequest  syncRequest = new SyncRequest(new SyncRequest.Collection(
                        store.GetMessagesSyncKey(session.Credential.UserName, folder), Clazz.Email, folder.Id, commands));
                SyncResponse response    = client.sync(session, syncRequest);
        
                if (response.Collections.Count == 0)
                    throw new DeltaSyncException("No <Collection> in Sync response");
                SyncResponse.Collection collection = response.Collections[0];
                if (collection.Status != 1) {
                    throw new DeltaSyncException("Sync request failed with status " 
                            + collection.Status);
                }
            
                if (collection.Commands != null && collection.Commands.Count > 0) {
                    // We don't send <GetChanges> so we shouldn't get any Commands in the response
                    store.ResetMessages(session.Credential.UserName, folder);
                    throw new DeltaSyncException("Delete should not return any Commands");
                }
                
                // Remove commands.size() elements from ids
                ids.RemoveRange(0, commands.Count);
                
                List<string> deletedInRun = new List<string>();
                foreach (SyncResponse.Collection.Response rsp in collection.Responses) {
                    if (rsp is SyncResponse.Collection.EmailDeleteResponse) {
                        SyncResponse.Collection.EmailDeleteResponse delRsp = 
                            (SyncResponse.Collection.EmailDeleteResponse) rsp;
                        deletedInRun.Add(delRsp.Id);
                        // status == 4403 means no such message found
                        if (delRsp.Status == 1) {
                            deletedInRun.Add(delRsp.Id);
                        }
                    }
                }
    
                deleted.AddRange(deletedInRun);
                
                store.UpdateMessages(session.Credential.UserName, folder, collection.SyncKey, 
                        new List<Message>(), deletedInRun);
            }
        }
        
        /**
         * Downloads the HU01 compressed content of the specified {@link Message} 
         * and writes it to the specified {@link Stream}.
         * 
         * @param message the {@link Message} to download the content for.
         * @param out the stream to write the HU01 compressed message content to.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public bool DownloadRawMessageContent(Message message, Stream output) 
        {
            checkLoggedIn();
            try {
                return client.DownloadRawMessageContent(session, message.Id, output);
            } catch (SessionExpiredException) {
                session = client.Renew(session);
                return client.DownloadRawMessageContent(session, message.Id, output);
            }
        }

        /**
         * Downloads the content of the specified {@link Message} and writes it to 
         * the specified {@link Stream}.
         * 
         * @param message the {@link Message} to download the content for.
         * @param out the stream to write the message content to.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public bool DownloadMessageContent(Message message, Stream output) {
            checkLoggedIn();
            try {
                return client.DownloadMessageContent(session, message.Id, output);
            } catch (SessionExpiredException) {
                session = client.Renew(session);
                return client.DownloadMessageContent(session, message.Id, output);
            }
        }
        
        /**
         * Downloads the content of the specified {@link Message} and writes it to 
         * the specified {@link Stream}.
         * 
         * @param message the {@link Message} to download the content for.
         * @param out the stream to write the message content to.
         * @throws SessionExpiredException if the session has expired and couldn't 
         *         be renewed.
         * @throws DeltaSyncException on errors returned by the server.
         * @throws IOException on communication errors.
         * @throws IllegalStateException if not logged in.
         */
        public T DownloadMessage<T>(Message message, DeltaSyncMessageParser<T> parser) where T : class {
            if (parser == null)
                throw new ArgumentNullException("MessageParser");
            checkLoggedIn();
            T result = null;
            try {
                client.DownloadMessageContent(session, message.Id, (Stream stream) => { result = parser(stream); });
            } catch (SessionExpiredException) {
                session = client.Renew(session);
                client.DownloadMessageContent(session, message.Id, (Stream stream) => { result = parser(stream); });
            }
            if (result == null)
                throw new MessageNotAvailableExeption("Program was unable to download the message: " + message.Id);
            return result;
        }
        
        #if !DONOTUSE_MimeKit
        public MimeMessage DownloadMessage(Message message) {
            return DownloadMessage<MimeMessage>(message, (Stream stream) => new MimeParser(stream).ParseMessage());
        }
        #endif
    }
}